-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix curlyline rendering in AtlasEngine
and GDIRenderer
#16444
Conversation
AtlasEngine
and GDIRender
AtlasEngine
and GDIRenderer
Thanks for fixing this! This is kinda strange, but... I am on a 150% scale display. In conhost, I am seeing a weird discontinuity in curly underline support! Consolas size 13No curly underline (as expected) Consolas size 14curly and double underlines appear consolas size 15 and 16Curlies go away again (!) consolas 17They're baa~ack |
(Double seems to disappear in GDI occasionally as well, or I can't get it to activate) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! I just got a couple questions (not those with BTW or FYI).
@@ -306,36 +306,30 @@ void BackendD3D::_updateFontDependents(const RenderingPayload& p) | |||
{ | |||
const auto& font = *p.s->font; | |||
|
|||
// The max height of Curly line peak in `em` units. | |||
const auto maxCurlyLinePeakHeightEm = 0.075f; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(FYI: This PR is tiny if you suppress whitespace changes.)
src/renderer/gdi/state.cpp
Outdated
// Since we use GDI pen for drawing, the underline offset should point to | ||
// the center of the underline. | ||
_lineMetrics.underlineOffset += strokeHalfWidth; | ||
_lineMetrics.underlineOffset2 += strokeHalfWidth; | ||
|
||
// We want the underline to always be visible and remain within the cell | ||
// bottom, so we clamp the offset to fit just inside. | ||
_lineMetrics.underlineOffset = std::min(_lineMetrics.underlineOffset, maxUnderlineOffset); | ||
_lineMetrics.underlineOffset2 = std::min(_lineMetrics.underlineOffset2, maxUnderlineOffset); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still seems wrong to me. The offsets have already been clamped prior to this, so no additional clamping should have been necessary. The strokeHalfWidth
adjustment should just be accounting for the difference between a PatBlt
rendering and a LineTo
rendering. If it was correct, then the LineTo
rendering should exactly match where the PatBlt
would have rendered without that adjustment. If it ends up out of bounds, then that's clearly not the case, so something must be wrong with the strokeHalfWidth
calculation. Maybe a rounding issue? I don't know.
Worst case, if you can't get it to work, at least move the strokeHalfWidth
adjustment higher up, prior to where we're doing the first clamping (and get rid of this second set of clamping). Because we've got special case handling for the double underline offset, which is potentially going to be broken if you clamp it a second time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think another potential problem is that the maxUnderlineOffset
that we're using for clamping is intended to be used with the original PatBlt
offsets. Once you add the strokeHalfWidth
adjustments, that range is no longer applicable. The only way I can see this working is if we can get that strokeHalfWidth
calculation correct.
I'd suggest writing a little test app that renders a bunch of lines of varying widths, using both LineTo
and PatBlt
. From that you should be able to figure out exactly what offset needs to be applied at each width to get the two renderings to match.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, I'm not well-versed in Windows programming in general, so creating an app from scratch would be a different task for me 😅
However, I did experiment with the idea of comparing LineTo
and PatBlt
, and it made it much more clear what I needed to do.
With this code:
// a fixed offset
const auto cellMidY = std::lround(fontHeight / 2.0f);
// adjust for drawing with DrawStrokedLine()
const auto cellMidYWithHalfStrokeWidth = cellMidY + gsl::narrow_cast<int>(std::floor(_lineMetrics.underlineWidth / 2));
// Select a brush
// draw a line with PatBlt()
RETURN_HR_IF(E_FAIL, !DrawLine(ptTarget.x, ptTarget.y + cellMidY, widthOfAllCells, _lineMetrics.underlineWidth));
// Select the dotted pen
// draw a line with LineTo()
RETURN_HR_IF(E_FAIL, !DrawStrokedLine(ptTarget.x, ptTarget.y + cellMidYWithHalfStrokeWidth, widthOfAllCells));
Result:
"`e[4:4m Dotted `e[m"
I tried this with other font sizes, and they all show the same result. So, the DrawLine()
and DrawStrokedLine()
calls with those args are synonymous to each other.
This still seems wrong to me. The offsets have already been clamped prior to this, so no additional clamping should have been necessary.
Yeah, you're right! It wouldn't have been necessary if I had floored strokeHalfWidth
before adding it to the two offsets. (I had lround
'd it, so that was the issue.)
That's probably due to how font metrics differ going from a small font size to larger ones. As an example, this table shows how transitioning from 13 to 15 font size in Conhost results in
And for a visual demo: |
I need to revisit all the calculations I did for the curly line in both renderers. Will push the updated code if they need to be fixed. |
I need you guys to give your thoughts on forcing curlyline height to be at least 1. What do you think should we do when there's not enough space to render a good Curlyline:
(2) has several benefits. Some fonts that otherwise would never draw a curlyline in case (1) (due to how close they draw underlines to the cell bottom) actually draw a curlyline that seemingly looks like there isn't any clipping involved (just don't look at them with eagle eyes). Curlyline is always distinguishable from single underline which is what the issuer requested in the linked issue
|
I am a big fan of (2), always making sure that we display a curly line. I know we can't move it upwards because that would mean it is discontinuous with the normal underline styles, so this seems totally sufficient to me! |
Ready for review (again)! |
if (maxDrawableCurlyLinePeakHeight > 0.0f) | ||
{ | ||
curlyLinePeakHeight = 0.0f; | ||
_lineMetrics.curlylinePeakHeight = std::min(_lineMetrics.curlylinePeakHeight, maxDrawableCurlyLinePeakHeight); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found that this behavior can look somewhat unpleasant when Ctrl+scrolling up and down when using Consolas, because it'll jump in between very squiggly lines and barely squiggly lines all the time. Additionally, and unrelated to your PR, I'm having trouble ever getting the double-underline to separate into two distinct lines.
So, I'm currently doing a couple modifications to this function to bring it closer to the corresponding AtlasEngine code: Using floats throughout the function, unless rounding is needed (= for instance, right before calculating the thin line width) and I'm porting Word's double underline algorithm over. My hope is that all this will allow me to better understand how your current code works and to come up with an idea for making the transition between curly and less-curly lines "smoother". (I'll submit the above changes in a different PR after this one merges.)
Personally speaking I don't agree with this:
I know we can't move it upwards because that would mean it is discontinuous with the normal underline styles, so this seems totally sufficient to me!
I think discontinuous lines are better, if it avoids clipping. But I also get that discontinuous lines are kind of weird too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, I'm currently doing a couple modifications to this function to bring it closer to the corresponding AtlasEngine code.....(I'll submit the above changes in a different PR after this one merges.)
That sounds good to me.
I have a few ideas that I'll just share here:
- We could give users an option to let WT/Conhost adjust underline-offfset. We'll make sure curlyline is always drawn perfectly. This is what Wezterm seems to do (can't be turned off).
- (Not sure if it works but) Have a separate render pass for drawing underlines after glyphs/background have been drawn. This ensures we can draw underlines at the right offset while avoiding any clipping. (Too much work)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm having trouble ever getting the double underline to separate into two distinct lines.
j4james briefly mentioned it in his comment in the original PR that brought doubly underlined rendering support:
Note how some fonts only have enough space for a thicker line, and not two distinct lines, but I think that's OK (XTerm always renders the attribute this way). Also note that this is more common in the GDI renderer because it uses a smaller line height.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just had a chat with Dustin, and we compared different approaches. Word's double-underline algorithm does look fairly well at small and big font sizes and works better with Consolas. Using the thinLineWidth
(as in AtlasEngine) for the curly underline also appears to be slightly better visually than the full underlineWidth
.
I'll approve this PR because it puts this code in a fantastic state to do further "fine tuning" on it. Thank you so much for working on this! I also didn't want to ask you to make any more changes, since I don't want to put any more unnecessary burden on you. I'll link the follow-up PR here and post screenshots there to compare the changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done: #16475
Thanks so much for doing this! |
While #16444 left wavy lines in an amazing state already, there were a few more things that could be done to make GDI look more consistent with other well known Windows applications. But before that, a couple unrelated, but helpful changes were made: * `GdiEngine::UpdateFont` was heavily modified to do all calculations in floats. All modern CPUs have fast FPUs and even the fairly slow `lroundf` function is so fast (relatively) nowadays that in a cold path like this, we can liberally call it to convert back to `int`s. This makes intermediate calculation more accurate and consistent. * `GdiEngine::PaintBufferGridLines` was exception-unsafe due to its use of a `std::vector` with catch clause and this PR fixes that. Additionally, the vector was swapped out with a `til::small_vector` to reduce heap allocations. (Arena allocators!) * RenderingTests was updated to cover styled underlines With that in place, these improvements were done: * Word's double-underline algorithm was ported over from `AtlasEngine`. It uses a half underline-width (aka `thinLineWidth`) which will now also be used for wavy lines to make them look a bit more filigrane. * The Bézier curve for wavy/curly underlines was modified to use control points at (0.5,0.5) and (0.5,-0.5) respectively. This results in a maxima at y=0.1414 which is much closer to a sine curve with a maxima at 1/(2pi) = 0.1592. Previously, the maxima was a lot higher (roughly 4x) depending on the aspect ratio of the glyphs. * Wavy underlines don't depend on the aspect ratio of glyphs anymore. This previously led to several problems depending on the exact font. The old renderer would draw exactly 3 periods of the wave into each cell which would also ensure continuity between cells. Unfortunately, this meant that waves could look inconsistent. The new approach always uses the aforementioned sine-like waves. * The wavy underline offset was clamped so that it's never outside of bounds of a line. This avoids clipping. ## Validation Steps Performed * Compile RenderingTests and run it * Using Consolas, MS Gothic and Cascadia Code while Ctrl+Scrolling up and down works as expected without clipping ✅
Fixes Curlyline being drawn as single underline in some cases **Detailed Description** - Curlyline is drawn at all font sizes. - We might render a curlyline that is clipped in cases where we don't have enough space to draw a full curlyline. This is to give users a consistent view of Curlylines. Previously in those cases, it was drawn as a single underline. - Removed minimum threshold `minCurlyLinePeakHeight` for Curlyline drawing. - GDIRender changes: - Underline offset now points to the (vertical) mid position of the underline. Removes redundant `underlineMidY` calculation inside the draw call. Closes #16288 (cherry picked from commit f5b45c2) Service-Card-Id: 91349182 Service-Version: 1.19
While #16444 left wavy lines in an amazing state already, there were a few more things that could be done to make GDI look more consistent with other well known Windows applications. But before that, a couple unrelated, but helpful changes were made: * `GdiEngine::UpdateFont` was heavily modified to do all calculations in floats. All modern CPUs have fast FPUs and even the fairly slow `lroundf` function is so fast (relatively) nowadays that in a cold path like this, we can liberally call it to convert back to `int`s. This makes intermediate calculation more accurate and consistent. * `GdiEngine::PaintBufferGridLines` was exception-unsafe due to its use of a `std::vector` with catch clause and this PR fixes that. Additionally, the vector was swapped out with a `til::small_vector` to reduce heap allocations. (Arena allocators!) * RenderingTests was updated to cover styled underlines With that in place, these improvements were done: * Word's double-underline algorithm was ported over from `AtlasEngine`. It uses a half underline-width (aka `thinLineWidth`) which will now also be used for wavy lines to make them look a bit more filigrane. * The Bézier curve for wavy/curly underlines was modified to use control points at (0.5,0.5) and (0.5,-0.5) respectively. This results in a maxima at y=0.1414 which is much closer to a sine curve with a maxima at 1/(2pi) = 0.1592. Previously, the maxima was a lot higher (roughly 4x) depending on the aspect ratio of the glyphs. * Wavy underlines don't depend on the aspect ratio of glyphs anymore. This previously led to several problems depending on the exact font. The old renderer would draw exactly 3 periods of the wave into each cell which would also ensure continuity between cells. Unfortunately, this meant that waves could look inconsistent. The new approach always uses the aforementioned sine-like waves. * The wavy underline offset was clamped so that it's never outside of bounds of a line. This avoids clipping. * Compile RenderingTests and run it * Using Consolas, MS Gothic and Cascadia Code while Ctrl+Scrolling up and down works as expected without clipping ✅ (cherry picked from commit 99193c9) Service-Card-Id: 91356394 Service-Version: 1.19
Fixes Curlyline being drawn as single underline in some cases
Detailed Description
minCurlyLinePeakHeight
for Curlyline drawing.underlineMidY
calculation inside the draw call.Closes #16288
Validation Steps Performed
PR Checklist